{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python Decorators"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### First day: quick howto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Decorators are a sometimes overlooked and more advanced feature. They support a nice way to abstract your code. Although hard to grasp at first there is not that much to it. Let's start with two definitions:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> A decorator is any callable Python object that is used to modify a function, method or class definition. A decorator is passed the original object being defined and returns a modified object, which is then bound to the name in the definition. - [PythonDecorators wiki](https://wiki.python.org/moin/PythonDecorators)\n",
    "\n",
    "> A decorator's intent is to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. - [Design Patterns book](https://www.amazon.com/dp/0201633612/?tag=pyb0f-20)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import wraps\n",
    "import time"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The basic template for a defining a decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "def mydecorator(function):\n",
    "    @wraps(function)\n",
    "    def wrapper(*args, **kwargs):\n",
    "        # do something before the original function is called\n",
    "        # call the passed in function\n",
    "        result = function(*args, **kwargs)\n",
    "        # do something after the original function call\n",
    "        return result\n",
    "    # return wrapper = decorated function\n",
    "    return wrapper"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can then use the decorator _wrapping_ your function like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "@mydecorator\n",
    "def my_function(args):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is just _syntactic sugar_ for:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def my_function(args):\n",
    "    pass\n",
    "\n",
    "my_function = mydecorator(my_function)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### args and kwargs detour"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note in Python there are different ways to pass arguments to functions, see this [great guide](http://docs.python-guide.org/en/latest/writing/style/#function-arguments) for more info and a quick example here:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_profile(name, active=True, *sports, **awards):\n",
    "    print('Positional arguments (required): ', name)\n",
    "    print('Keyword arguments (not required, default values): ', active)\n",
    "    print('Arbitrary argument list (sports): ', sports)\n",
    "    print('Arbitrary keyword argument dictionary (awards): ', awards)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "get_profile() missing 1 required positional argument: 'name'",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-6-2ef2bb648fe3>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_profile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m: get_profile() missing 1 required positional argument: 'name'"
     ]
    }
   ],
   "source": [
    "get_profile()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Positional arguments (required):  julian\n",
      "Keyword arguments (not required, default values):  True\n",
      "Arbitrary argument list (sports):  ()\n",
      "Arbitrary keyword argument dictionary (awards):  {}\n"
     ]
    }
   ],
   "source": [
    "get_profile('julian')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Positional arguments (required):  julian\n",
      "Keyword arguments (not required, default values):  False\n",
      "Arbitrary argument list (sports):  ()\n",
      "Arbitrary keyword argument dictionary (awards):  {}\n"
     ]
    }
   ],
   "source": [
    "get_profile('julian', active=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Positional arguments (required):  julian\n",
      "Keyword arguments (not required, default values):  False\n",
      "Arbitrary argument list (sports):  ('basketball', 'soccer')\n",
      "Arbitrary keyword argument dictionary (awards):  {}\n"
     ]
    }
   ],
   "source": [
    "get_profile('julian', False, 'basketball', 'soccer')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Positional arguments (required):  julian\n",
      "Keyword arguments (not required, default values):  False\n",
      "Arbitrary argument list (sports):  ('basketball', 'soccer')\n",
      "Arbitrary keyword argument dictionary (awards):  {'pythonista': 'special honor of the community', 'topcoder': '2017 code camp'}\n"
     ]
    }
   ],
   "source": [
    "get_profile('julian', False, 'basketball', 'soccer',\n",
    "            pythonista='special honor of the community', topcoder='2017 code camp')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_args(function):\n",
    "    @wraps(function)\n",
    "    def wrapper(*args, **kwargs):     \n",
    "        print('hi from decorator - args:')\n",
    "        print(args)\n",
    "        result = function(*args, **kwargs)\n",
    "        print('hi again from decorator - kwargs:')\n",
    "        print(kwargs)\n",
    "        return result\n",
    "    # return wrapper as a decorated function\n",
    "    return wrapper"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "@show_args\n",
    "def get_profile(name, active=True, *sports, **awards):\n",
    "    print('\\n\\thi from the get_profile function\\n')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "hi from decorator - args:\n",
      "('bob', True, 'basketball', 'soccer')\n",
      "\n",
      "\thi from the get_profile function\n",
      "\n",
      "hi again from decorator - kwargs:\n",
      "{'pythonista': 'special honor of the community', 'topcoder': '2017 code camp'}\n"
     ]
    }
   ],
   "source": [
    "get_profile('bob', True, 'basketball', 'soccer', \n",
    "            pythonista='special honor of the community', topcoder='2017 code camp')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Timing a function / wraps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's look at a more practical/ realistic example, say you want to time a function's execution:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def timeit(func):\n",
    "    '''Decorator to time a function'''\n",
    "    @wraps(func)\n",
    "    def wrapper(*args, **kwargs):\n",
    "        \n",
    "        # before calling the decorated function\n",
    "        print('== starting timer')\n",
    "        start = time.time()\n",
    "        \n",
    "        # call the decorated function\n",
    "        func(*args, **kwargs)\n",
    "        \n",
    "        # after calling the decorated function\n",
    "        end = time.time()\n",
    "        print(f'== {func.__name__} took {int(end-start)} seconds to complete')\n",
    "    \n",
    "    return wrapper"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> It's important to add the [`functools.wraps`](https://docs.python.org/3.7/library/functools.html#functools.wraps) decorator to preserve the original function name and docstring. For example if we take `@wraps` out of `timeit` `timeit.__name__` would return `wrapper` and `timeit.__doc__` would be empty (lost docstring)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And here is the function we will decorate next:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(actual function) Done, report links ...\n"
     ]
    }
   ],
   "source": [
    "def generate_report():\n",
    "    '''Function to generate revenue report'''\n",
    "    time.sleep(2)\n",
    "    print('(actual function) Done, report links ...')\n",
    "\n",
    "generate_report()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now see what happens if we wrap `timeit` around it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "== starting timer\n",
      "(actual function) Done, report links ...\n",
      "== generate_report took 2 seconds to complete\n"
     ]
    }
   ],
   "source": [
    "@timeit\n",
    "def generate_report():\n",
    "    '''Function to generate revenue report'''\n",
    "    time.sleep(2)\n",
    "    print('(actual function) Done, report links ...')\n",
    "\n",
    "generate_report()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`@wraps(func)` preserved the docstring:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Function to generate revenue report'"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "generate_report.__doc__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Stacking decorators"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src='stacking.png' style='float:left;'>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Decorators can be stacked, let's define another one, for example to print (positional) `args` and (keyword) `kwargs` of the function that is passed in:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "def print_args(func):\n",
    "    '''Decorator to print function arguments'''\n",
    "    @wraps(func)\n",
    "    def wrapper(*args, **kwargs):\n",
    "        \n",
    "        # before\n",
    "        print()\n",
    "        print('*** args:')\n",
    "        for arg in args:\n",
    "            print(f'- {arg}')\n",
    "        \n",
    "        print('**** kwargs:')\n",
    "        for k, v in kwargs.items():\n",
    "            print(f'- {k}: {v}')\n",
    "        print()\n",
    "        \n",
    "        # call func\n",
    "        func(*args, **kwargs)\n",
    "    return wrapper"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's modify our `generate_report` function to take args:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_report(*months, **parameters):\n",
    "    time.sleep(2)\n",
    "    print('(actual function) Done, report links ...')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's add our `print_args` decorator on top of the `timeit` one and call `generate_report` with some arguments. Note that the order matters here, make sure `timeit` is the __outer__ decorator so the output starts with _== starting timer_:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "@timeit\n",
    "@print_args\n",
    "def generate_report(*months, **parameters):\n",
    "    time.sleep(2)\n",
    "    print('(actual function) Done, report links ...')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "parameters = dict(split_geos=True, include_suborgs=False, tax_rate=33)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "== starting timer\n",
      "\n",
      "*** args:\n",
      "- October\n",
      "- November\n",
      "- December\n",
      "**** kwargs:\n",
      "- split_geos: True\n",
      "- include_suborgs: False\n",
      "- tax_rate: 33\n",
      "\n",
      "(actual function) Done, report links ...\n",
      "== generate_report took 2 seconds to complete\n"
     ]
    }
   ],
   "source": [
    "generate_report('October', 'November', 'December', **parameters)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Common use cases / further reading"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For [Never Forget A Friend’s Birthday with Python, Flask and Twilio](https://www.twilio.com/blog/2017/09/never-forget-friends-birthday-python-flask-twilio.html) I used a [decorator](https://github.com/pybites/bday-app/blob/a360a02316e021ac4c3164dcdc4122da5d5a722b/app.py#L28) to check if a user is logged in, loosely based on the one provided in the [Flask documentation](https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/#login-required-decorator). \n",
    "\n",
    "Another interesting one to check out (if you still have some time to squeeze in today): Django's `login_required` decorator - [source](https://github.com/django/django/blob/master/django/contrib/auth/decorators.py)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For more use cases see the _Decorators in the wild_ section of our [Learning Python Decorators by Example](https://pybit.es/decorators-by-example.html) article. At the end of that article there are links to more material including some more advanced use cases (e.g. decorators with optional arguments)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Second day: a practical exercise"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Head over to [Bite 22. Write a decorator with argument](https://codechalleng.es/bites/promo/decorator-fun) and try write the `make_html` decorator to make this work:\n",
    "\n",
    "    @make_html('p')\n",
    "    @make_html('strong')\n",
    "    def get_text(text):\n",
    "        print(text)\n",
    "\n",
    "Calling:\n",
    "\n",
    "    get_text('I code with PyBites')\n",
    "\n",
    "Should return:\n",
    "\n",
    "    <p><strong>I code with PyBites</strong></p>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Third day: more practice "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Take PyBites [Write DRY Code With Decorators blog challenge](https://codechalleng.es/challenges/14/) and write one or more decorators of your choice. \n",
    "\n",
    "Look at the code you have written so far, where could you refactor / add decorators? The more you practice the sooner you grok them and the easier they become. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Time to share what you've accomplished!\n",
    "\n",
    "Be sure to share your last couple of days work on Twitter or Facebook. Use the hashtag **#100DaysOfCode**. \n",
    "\n",
    "Here are [some examples](https://twitter.com/search?q=%23100DaysOfCode) to inspire you. Consider including [@talkpython](https://twitter.com/talkpython) and [@pybites](https://twitter.com/pybites) in your tweets.\n",
    "\n",
    "*See a mistake in these instructions? Please [submit a new issue](https://github.com/talkpython/100daysofcode-with-python-course/issues) or fix it and [submit a PR](https://github.com/talkpython/100daysofcode-with-python-course/pulls).*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "venv",
   "language": "python",
   "name": "venv"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
